Micron Document
πŸŽ–οΈGitΠ―Ρ€Π°πŸŽ–οΈ


Displaying Raw β€’ Download

core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/BleRadioInterface.kt e23fab266758cd1fcacaf55e5cf25aea4c5cf00e (e23fab26) Text, 26.20 KB

T8b949e/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tf0883e@fileTb4b4b4:Te6edf3SuppressTb4b4b4(Ta5d6ff"Ta5d6ffTooManyFunctionsTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffTooGenericExceptionCaughtTa5d6ff"Tb4b4b4)

Tff7b72package T7ee787org.meshtastic.core.network.radio

Tff7b72import T7ee787co.touchlab.kermit.Logger
Tff7b72import T7ee787kotlinx.coroutines.CoroutineExceptionHandler
Tff7b72import T7ee787kotlinx.coroutines.CoroutineScope
Tff7b72import T7ee787kotlinx.coroutines.DelicateCoroutinesApi
Tff7b72import T7ee787kotlinx.coroutines.GlobalScope
Tff7b72import T7ee787kotlinx.coroutines.Job
Tff7b72import T7ee787kotlinx.coroutines.SupervisorJob
Tff7b72import T7ee787kotlinx.coroutines.cancel
Tff7b72import T7ee787kotlinx.coroutines.coroutineScope
Tff7b72import T7ee787kotlinx.coroutines.delay
Tff7b72import T7ee787kotlinx.coroutines.flow.catch
Tff7b72import T7ee787kotlinx.coroutines.flow.first
Tff7b72import T7ee787kotlinx.coroutines.flow.launchIn
Tff7b72import T7ee787kotlinx.coroutines.flow.onEach
Tff7b72import T7ee787kotlinx.coroutines.isActive
Tff7b72import T7ee787kotlinx.coroutines.job
Tff7b72import T7ee787kotlinx.coroutines.launch
Tff7b72import T7ee787kotlinx.coroutines.sync.Mutex
Tff7b72import T7ee787kotlinx.coroutines.sync.withLock
Tff7b72import T7ee787kotlinx.coroutines.withTimeoutOrNull
Tff7b72import T7ee787org.meshtastic.core.ble.BleConnection
Tff7b72import T7ee787org.meshtastic.core.ble.BleConnectionFactory
Tff7b72import T7ee787org.meshtastic.core.ble.BleConnectionState
Tff7b72import T7ee787org.meshtastic.core.ble.BleDevice
Tff7b72import T7ee787org.meshtastic.core.ble.BleScanner
Tff7b72import T7ee787org.meshtastic.core.ble.BleWriteType
Tff7b72import T7ee787org.meshtastic.core.ble.BluetoothRepository
Tff7b72import T7ee787org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID
Tff7b72import T7ee787org.meshtastic.core.ble.retryBleOperation
Tff7b72import T7ee787org.meshtastic.core.ble.toMeshtasticRadioProfile
Tff7b72import T7ee787org.meshtastic.core.common.util.nowMillis
Tff7b72import T7ee787org.meshtastic.core.model.RadioNotConnectedException
Tff7b72import T7ee787org.meshtastic.core.repository.RadioInterfaceService
Tff7b72import T7ee787org.meshtastic.core.repository.RadioTransport
Tff7b72import T7ee787org.meshtastic.proto.Heartbeat
Tff7b72import T7ee787org.meshtastic.proto.ToRadio
Tff7b72import T7ee787kotlin.concurrent.Volatile
Tff7b72import T7ee787kotlin.concurrent.atomics.AtomicInt
Tff7b72import T7ee787kotlin.concurrent.atomics.ExperimentalAtomicApi
Tff7b72import T7ee787kotlin.time.Duration.Companion.seconds

Tff7b72private Tff7b72const Tff7b72val Te6edf3SCAN_RETRY_COUNT Tff7b72= T79c0ff3
Tff7b72private Tff7b72const Tff7b72val Te6edf3SCAN_RETRY_DELAY_MS Tff7b72= T79c0ff1T79c0ff0T79c0ff0T79c0ff0L
Tff7b72private Tff7b72const Tff7b72val Te6edf3CONNECTION_TIMEOUT_MS Tff7b72= T79c0ff1T79c0ff5Te6edf3_000L
Tff7b72private Tff7b72const Tff7b72val Te6edf3RECONNECT_FAILURE_THRESHOLD Tff7b72= T79c0ff3
Tff7b72private Tff7b72const Tff7b72val Te6edf3RECONNECT_BASE_DELAY_MS Tff7b72= T79c0ff5Te6edf3_000L
Tff7b72private Tff7b72const Tff7b72val Te6edf3RECONNECT_MAX_DELAY_MS Tff7b72= T79c0ff6T79c0ff0Te6edf3_000L
Tff7b72private Tff7b72const Tff7b72val Te6edf3RECONNECT_MAX_FAILURES Tff7b72= T79c0ff1T79c0ff0

T8b949e/**
* Minimum milliseconds a BLE connection must stay up before we consider it "stable" and reset
* [BleRadioInterface.consecutiveFailures]. Without this, a device at the edge of BLE range can repeatedly connect for a
* fraction of a second and drop β€” each brief connection resets the failure counter so [RECONNECT_FAILURE_THRESHOLD] is
* never reached, and the app never signals [ConnectionState.DeviceSleep].
*
* The value (5 s) is long enough that only connections that survive past the initial GATT setup are treated as genuine,
* but short enough that normal reconnects after light-sleep still reset the counter promptly.
*/
Tff7b72private Tff7b72const Tff7b72val Te6edf3MIN_STABLE_CONNECTION_MS Tff7b72= T79c0ff5Te6edf3_000L

T8b949e/**
* Returns the reconnect backoff delay in milliseconds for a given consecutive failure count.
*
* Backoff schedule: 1 failure β†’ 5 s 2 failures β†’ 10 s 3 failures β†’ 20 s 4 failures β†’ 40 s 5+ failures β†’ 60 s (capped)
*/
Tff7b72internal Tff7b72fun Td2a8ffcomputeReconnectBackoffMsTb4b4b4(Te6edf3consecutiveFailuresTb4b4b4: Tffa657IntTb4b4b4)Tb4b4b4: Tffa657Long Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3consecutiveFailures Tff7b72<Tff7b72= T79c0ff0Tb4b4b4) Tff7b72return Te6edf3RECONNECT_BASE_DELAY_MS
Tff7b72return Te6edf3minOfTb4b4b4(Te6edf3RECONNECT_BASE_DELAY_MS Tff7b72* Tb4b4b4(T79c0ff1L Te6edf3shl Tb4b4b4(Te6edf3consecutiveFailures Tff7b72- T79c0ff1Tb4b4b4)Tb4b4b4.Te6edf3coerceAtMostTb4b4b4(T79c0ff4Tb4b4b4)Tb4b4b4)Tb4b4b4, Te6edf3RECONNECT_MAX_DELAY_MSTb4b4b4)
Tb4b4b4}

T8b949e// Milliseconds to wait after launching characteristic observations before triggering the
T8b949e// Meshtastic handshake. Both fromRadio and logRadio observation flows write the CCCD
T8b949e// asynchronously via Kable's GATT queue. Without this settle window the want_config_id
T8b949e// burst from the radio can arrive before notifications are enabled, causing the first
T8b949e// handshake attempt to look like a stall.
Tff7b72private Tff7b72const Tff7b72val Te6edf3CCCD_SETTLE_MS Tff7b72= T79c0ff5T79c0ff0L

Tff7b72private Tff7b72val Te6edf3SCAN_TIMEOUT Tff7b72= T79c0ff5.Te6edf3seconds
Tff7b72private Tff7b72val Te6edf3GATT_CLEANUP_TIMEOUT Tff7b72= T79c0ff5.Te6edf3seconds

T8b949e/**
* A [RadioTransport] implementation for BLE devices using the common BLE abstractions (which are powered by Kable).
*
* This class handles the high-level connection lifecycle for Meshtastic radios over BLE, including:
* - Bonding and discovery.
* - Automatic reconnection logic.
* - MTU and connection parameter monitoring.
* - Routing raw byte packets between the radio and [RadioInterfaceService].
*
* @param serviceScope The coroutine scope to use for launching coroutines.
* @param scanner The BLE scanner.
* @param bluetoothRepository The Bluetooth repository.
* @param connectionFactory The BLE connection factory.
* @param service The [RadioInterfaceService] to use for handling radio events.
* @param address The BLE address of the device to connect to.
*/
Tff7b72class T56d364BleRadioInterfaceTb4b4b4(
Tff7b72private Tff7b72val Te6edf3serviceScopeTb4b4b4: Te6edf3CoroutineScopeTb4b4b4,
Tff7b72private Tff7b72val Te6edf3scannerTb4b4b4: Te6edf3BleScannerTb4b4b4,
Tff7b72private Tff7b72val Te6edf3bluetoothRepositoryTb4b4b4: Te6edf3BluetoothRepositoryTb4b4b4,
Tff7b72private Tff7b72val Te6edf3connectionFactoryTb4b4b4: Te6edf3BleConnectionFactoryTb4b4b4,
Tff7b72private Tff7b72val Te6edf3serviceTb4b4b4: Te6edf3RadioInterfaceServiceTb4b4b4,
Tff7b72val Te6edf3addressTb4b4b4: Tffa657StringTb4b4b4,
Tb4b4b4) Tb4b4b4: Te6edf3RadioTransport Tb4b4b4{

Tff7b72private Tff7b72val Te6edf3exceptionHandler Tff7b72= Te6edf3CoroutineExceptionHandler Tb4b4b4{ Te6edf3_Tb4b4b4, Te6edf3throwable Tff7b72-Tff7b72>
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3throwableTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Uncaught exception in connectionScopeTa5d6ff" Tb4b4b4}
Te6edf3serviceScopeTb4b4b4.Te6edf3launch Tb4b4b4{
Tff7b72try Tb4b4b4{
Te6edf3bleConnectionTb4b4b4.Te6edf3disconnectTb4b4b4(Tb4b4b4)
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Failed to disconnect in exception handlerTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tff7b72val Tb4b4b4(Te6edf3isPermanentTb4b4b4, Te6edf3msgTb4b4b4) Tff7b72= Te6edf3throwableTb4b4b4.Te6edf3toDisconnectReasonTb4b4b4(Tb4b4b4)
Te6edf3serviceTb4b4b4.Te6edf3onDisconnectTb4b4b4(Te6edf3isPermanentTb4b4b4, Te6edf3errorMessage Tff7b72= Te6edf3msgTb4b4b4)
Tb4b4b4}

Tff7b72private Tff7b72val Te6edf3connectionScopeTb4b4b4: Te6edf3CoroutineScope Tff7b72=
Te6edf3CoroutineScopeTb4b4b4(
Te6edf3serviceScopeTb4b4b4.Te6edf3coroutineContext Tff7b72+ Te6edf3SupervisorJobTb4b4b4(Te6edf3serviceScopeTb4b4b4.Te6edf3coroutineContextTb4b4b4.Te6edf3jobTb4b4b4) Tff7b72+ Te6edf3exceptionHandlerTb4b4b4,
Tb4b4b4)
Tff7b72private Tff7b72val Te6edf3bleConnectionTb4b4b4: Te6edf3BleConnection Tff7b72= Te6edf3connectionFactoryTb4b4b4.Te6edf3createTb4b4b4(Te6edf3connectionScopeTb4b4b4, Te6edf3addressTb4b4b4)
Tff7b72private Tff7b72val Te6edf3writeMutexTb4b4b4: Te6edf3Mutex Tff7b72= Te6edf3MutexTb4b4b4(Tb4b4b4)

Tff7b72private Tff7b72var Te6edf3connectionStartTimeTb4b4b4: Tffa657Long Tff7b72= T79c0ff0
Tff7b72private Tff7b72var Te6edf3packetsReceivedTb4b4b4: Tffa657Int Tff7b72= T79c0ff0
Tff7b72private Tff7b72var Te6edf3packetsSentTb4b4b4: Tffa657Int Tff7b72= T79c0ff0
Tff7b72private Tff7b72var Te6edf3bytesReceivedTb4b4b4: Tffa657Long Tff7b72= T79c0ff0
Tff7b72private Tff7b72var Te6edf3bytesSentTb4b4b4: Tffa657Long Tff7b72= T79c0ff0

Tf0883e@Volatile Tff7b72private Tff7b72var Te6edf3isFullyConnected Tff7b72= Tff7b72false
Tff7b72private Tff7b72var Te6edf3connectionJobTb4b4b4: Te6edf3Job? Tff7b72= Tff7b72null
Tff7b72private Tff7b72var Te6edf3consecutiveFailures Tff7b72= T79c0ff0

Tf0883e@OptInTb4b4b4(Te6edf3ExperimentalAtomicApiTff7b72::Te6edf3classTb4b4b4)
Tff7b72private Tff7b72val Te6edf3heartbeatNonce Tff7b72= Te6edf3AtomicIntTb4b4b4(T79c0ff0Tb4b4b4)

Tff7b72init Tb4b4b4{
Te6edf3connectTb4b4b4(Tb4b4b4)
Tb4b4b4}

T8b949e// --- Connection & Discovery Logic ---

T8b949e/** Robustly finds the device. First checks bonded devices, then performs a short scan if not found. */
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8fffindDeviceTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3BleDevice Tb4b4b4{
Te6edf3bluetoothRepositoryTb4b4b4.Te6edf3stateTb4b4b4.Te6edf3valueTb4b4b4.Te6edf3bondedDevices
Tb4b4b4.Te6edf3firstOrNull Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3addressTb4b4b4.Te6edf3equalsTb4b4b4(Te6edf3addressTb4b4b4, Te6edf3ignoreCase Tff7b72= Tff7b72trueTb4b4b4) Tb4b4b4}
Tff7b72?.Te6edf3let Tb4b4b4{
Tff7b72return Tffa657it
Tb4b4b4}

Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Device not found in bonded list, scanningTa5d6ff" Tb4b4b4}

Te6edf3repeatTb4b4b4(Te6edf3SCAN_RETRY_COUNTTb4b4b4) Tb4b4b4{ Te6edf3attempt Tff7b72-Tff7b72>
Tff7b72try Tb4b4b4{
Tff7b72val Te6edf3d Tff7b72=
Te6edf3withTimeoutOrNullTb4b4b4(Te6edf3SCAN_TIMEOUTTb4b4b4) Tb4b4b4{
Te6edf3scannerTb4b4b4.Te6edf3scanTb4b4b4(Te6edf3timeout Tff7b72= Te6edf3SCAN_TIMEOUTTb4b4b4, Te6edf3serviceUuid Tff7b72= Te6edf3SERVICE_UUIDTb4b4b4, Te6edf3address Tff7b72= Te6edf3addressTb4b4b4)Tb4b4b4.Te6edf3first Tb4b4b4{
Tffa657itTb4b4b4.Te6edf3addressTb4b4b4.Te6edf3equalsTb4b4b4(Te6edf3addressTb4b4b4, Te6edf3ignoreCase Tff7b72= Tff7b72trueTb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tff7b72if Tb4b4b4(Te6edf3d Tff7b72!Tff7b72= Tff7b72nullTb4b4b4) Tff7b72return Te6edf3d
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3vTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Scan attempt failed or timed outTa5d6ff" Tb4b4b4}
Tb4b4b4}

Tff7b72if Tb4b4b4(Te6edf3attempt Tff7b72< Te6edf3SCAN_RETRY_COUNT Tff7b72- T79c0ff1Tb4b4b4) Tb4b4b4{
Te6edf3delayTb4b4b4(Te6edf3SCAN_RETRY_DELAY_MSTb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tff7b72throw Te6edf3RadioNotConnectedExceptionTb4b4b4(Ta5d6ff"Ta5d6ffDevice not found at address Tffd700$Te6edf3addressTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffLongMethodTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffCyclomaticComplexMethodTa5d6ff"Tb4b4b4)
Tff7b72private Tff7b72fun Td2a8ffconnectTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3connectionJob Tff7b72=
Te6edf3connectionScopeTb4b4b4.Te6edf3launch Tb4b4b4{
Tff7b72while Tb4b4b4(Te6edf3isActiveTb4b4b4) Tb4b4b4{
Tff7b72try Tb4b4b4{
T8b949e// Allow any pending background disconnects to complete and the Android BLE stack
T8b949e// to settle before we attempt a new connection.
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffMagicNumberTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3connectDelayMs Tff7b72= T79c0ff1T79c0ff0T79c0ff0T79c0ff0L
Te6edf3delayTb4b4b4(Te6edf3connectDelayMsTb4b4b4)

Te6edf3connectionStartTime Tff7b72= Te6edf3nowMillis
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] BLE connection attempt startedTa5d6ff" Tb4b4b4}

Tff7b72val Te6edf3device Tff7b72= Te6edf3findDeviceTb4b4b4(Tb4b4b4)

T8b949e// Ensure the device is bonded before connecting. On Android, the
T8b949e// firmware may require an encrypted link (pairing mode != NO_PIN).
T8b949e// Without an explicit bond the GATT connection will fail with
T8b949e// insufficient-authentication (status 5) or the dreaded status 133.
T8b949e// On Desktop/JVM this is a no-op since the OS handles pairing during
T8b949e// the GATT connection when the peripheral requires it.
Tff7b72if Tb4b4b4(Tff7b72!Te6edf3bluetoothRepositoryTb4b4b4.Te6edf3isBondedTb4b4b4(Te6edf3addressTb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Device not bonded, initiating bondingTa5d6ff" Tb4b4b4}
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffTooGenericExceptionCaughtTa5d6ff"Tb4b4b4)
Tff7b72try Tb4b4b4{
Te6edf3bluetoothRepositoryTb4b4b4.Te6edf3bondTb4b4b4(Te6edf3deviceTb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Bonding successfulTa5d6ff" Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Bonding failed, attempting connection anywayTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

Tff7b72var Te6edf3state Tff7b72= Te6edf3bleConnectionTb4b4b4.Te6edf3connectAndAwaitTb4b4b4(Te6edf3deviceTb4b4b4, Te6edf3CONNECTION_TIMEOUT_MSTb4b4b4)

Tff7b72if Tb4b4b4(Te6edf3state Tff7b72!is Te6edf3BleConnectionStateTb4b4b4.Te6edf3ConnectedTb4b4b4) Tb4b4b4{
T8b949e// Kable on Android occasionally fails the first connection attempt with
T8b949e// NotConnectedException if the previous peripheral wasn't fully cleaned
T8b949e// up by the OS. A quick retry resolves it.
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] First connection attempt failed, retrying in 1.5sTa5d6ff" Tb4b4b4}
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffMagicNumberTa5d6ff"Tb4b4b4)
Te6edf3delayTb4b4b4(T79c0ff1T79c0ff5T79c0ff0T79c0ff0LTb4b4b4)
Te6edf3state Tff7b72= Te6edf3bleConnectionTb4b4b4.Te6edf3connectAndAwaitTb4b4b4(Te6edf3deviceTb4b4b4, Te6edf3CONNECTION_TIMEOUT_MSTb4b4b4)
Tb4b4b4}

Tff7b72if Tb4b4b4(Te6edf3state Tff7b72!is Te6edf3BleConnectionStateTb4b4b4.Te6edf3ConnectedTb4b4b4) Tb4b4b4{
Tff7b72throw Te6edf3RadioNotConnectedExceptionTb4b4b4(Ta5d6ff"Ta5d6ffFailed to connect to device at address Tffd700$Te6edf3addressTa5d6ff"Tb4b4b4)
Tb4b4b4}

T8b949e// Connection succeeded β€” only reset the failure counter if the
T8b949e// connection stays up long enough. See MIN_STABLE_CONNECTION_MS.
Tff7b72val Te6edf3gattConnectedAt Tff7b72= Te6edf3nowMillis
Te6edf3isFullyConnected Tff7b72= Tff7b72true
Te6edf3onConnectedTb4b4b4(Tb4b4b4)

T8b949e// Use coroutineScope so that the connectionState listener is scoped to this
T8b949e// iteration only. When the inner scope exits (on disconnect), the listener is
T8b949e// cancelled automatically before the next reconnect cycle starts a fresh one.
Te6edf3coroutineScope Tb4b4b4{
Te6edf3bleConnectionTb4b4b4.Te6edf3connectionState
Tb4b4b4.Te6edf3onEach Tb4b4b4{ Te6edf3s Tff7b72-Tff7b72>
Tff7b72if Tb4b4b4(Te6edf3s Tff7b72is Te6edf3BleConnectionStateTb4b4b4.Te6edf3Disconnected Tff7b72&Tff7b72& Te6edf3isFullyConnectedTb4b4b4) Tb4b4b4{
Te6edf3isFullyConnected Tff7b72= Tff7b72false
Te6edf3onDisconnectedTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4.Te6edf3catch Tb4b4b4{ Te6edf3e Tff7b72-Tff7b72> Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] bleConnection.connectionState flow crashedTa5d6ff" Tb4b4b4} Tb4b4b4}
Tb4b4b4.Te6edf3launchInTb4b4b4(Tff7b72thisTb4b4b4)

Te6edf3discoverServicesAndSetupCharacteristicsTb4b4b4(Tb4b4b4)

T8b949e// Suspend here until Kable drops the connection
Te6edf3bleConnectionTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3first Tb4b4b4{ Tffa657it Tff7b72is Te6edf3BleConnectionStateTb4b4b4.Te6edf3Disconnected Tb4b4b4}
Tb4b4b4}

Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] BLE connection dropped, preparing to reconnectTa5d6ff" Tb4b4b4}

T8b949e// Only reset the failure counter if the connection was stable (lasted
T8b949e// longer than MIN_STABLE_CONNECTION_MS). A connection that drops within
T8b949e// seconds typically means the device is at the edge of BLE range or
T8b949e// powered off β€” the Android BLE stack may briefly "connect" to a cached
T8b949e// GATT profile before realising the device is gone. Without this guard,
T8b949e// the failure counter resets on every brief connect, preventing us from
T8b949e// ever reaching RECONNECT_FAILURE_THRESHOLD and signalling DeviceSleep.
Tff7b72val Te6edf3connectionUptime Tff7b72= Te6edf3nowMillis Tff7b72- Te6edf3gattConnectedAt
Tff7b72if Tb4b4b4(Te6edf3connectionUptime Tff7b72>Tff7b72= Te6edf3MIN_STABLE_CONNECTION_MSTb4b4b4) Tb4b4b4{
Te6edf3consecutiveFailures Tff7b72= T79c0ff0
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3consecutiveFailuresTff7b72+Tff7b72+
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{
Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Connection lasted only Tffd700${Te6edf3connectionUptimeTffd700}Ta5d6ffms Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ff(< Tffd700${Te6edf3MIN_STABLE_CONNECTION_MSTffd700}Ta5d6ffms) β€” treating as failure Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ff(consecutive failures: Tffd700$Te6edf3consecutiveFailuresTa5d6ff)Ta5d6ff"
Tb4b4b4}
Tff7b72if Tb4b4b4(Te6edf3consecutiveFailures Tff7b72>Tff7b72= Te6edf3RECONNECT_MAX_FAILURESTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3e Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Giving up after Tffd700$Te6edf3consecutiveFailuresTa5d6ff unstable connectionsTa5d6ff" Tb4b4b4}
Te6edf3serviceTb4b4b4.Te6edf3onDisconnectTb4b4b4(
Te6edf3isPermanent Tff7b72= Tff7b72trueTb4b4b4,
Te6edf3errorMessage Tff7b72= Ta5d6ff"Ta5d6ffDevice unreachable (unstable connection)Ta5d6ff"Tb4b4b4,
Tb4b4b4)
Tff7b72returnTf0883e@launch
Tb4b4b4}
Tff7b72if Tb4b4b4(Te6edf3consecutiveFailures Tff7b72>Tff7b72= Te6edf3RECONNECT_FAILURE_THRESHOLDTb4b4b4) Tb4b4b4{
Te6edf3serviceTb4b4b4.Te6edf3onDisconnectTb4b4b4(
Te6edf3isPermanent Tff7b72= Tff7b72falseTb4b4b4,
Te6edf3errorMessage Tff7b72= Ta5d6ff"Ta5d6ffDevice unreachable (unstable connection)Ta5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3kotlinxTb4b4b4.Te6edf3coroutinesTb4b4b4.Te6edf3CancellationExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] BLE connection coroutine cancelledTa5d6ff" Tb4b4b4}
Tff7b72throw Te6edf3e
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3failureTime Tff7b72= Te6edf3nowMillis Tff7b72- Te6edf3connectionStartTime
Te6edf3consecutiveFailuresTff7b72+Tff7b72+
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{
Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Failed to connect to device after Tffd700${Te6edf3failureTimeTffd700}Ta5d6ffms Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ff(consecutive failures: Tffd700$Te6edf3consecutiveFailuresTa5d6ff)Ta5d6ff"
Tb4b4b4}

T8b949e// After exceeding the max failure limit, give up permanently to stop
T8b949e// draining battery on a device that is genuinely offline. The user
T8b949e// must manually reconnect from the connections screen.
Tff7b72if Tb4b4b4(Te6edf3consecutiveFailures Tff7b72>Tff7b72= Te6edf3RECONNECT_MAX_FAILURESTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3e Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Giving up after Tffd700$Te6edf3consecutiveFailuresTa5d6ff consecutive failuresTa5d6ff" Tb4b4b4}
Tff7b72val Tb4b4b4(Te6edf3_Tb4b4b4, Te6edf3msgTb4b4b4) Tff7b72= Te6edf3eTb4b4b4.Te6edf3toDisconnectReasonTb4b4b4(Tb4b4b4)
Te6edf3serviceTb4b4b4.Te6edf3onDisconnectTb4b4b4(Te6edf3isPermanent Tff7b72= Tff7b72trueTb4b4b4, Te6edf3errorMessage Tff7b72= Te6edf3msgTb4b4b4)
Tff7b72returnTf0883e@launch
Tb4b4b4}

T8b949e// At the failure threshold, signal DeviceSleep so
T8b949e// MeshConnectionManagerImpl can start its sleep timeout.
Tff7b72if Tb4b4b4(Te6edf3consecutiveFailures Tff7b72>Tff7b72= Te6edf3RECONNECT_FAILURE_THRESHOLDTb4b4b4) Tb4b4b4{
Te6edf3handleFailureTb4b4b4(Te6edf3eTb4b4b4)
Tb4b4b4}

T8b949e// Exponential backoff: 5s β†’ 10s β†’ 20s β†’ 40s β†’ capped at 60s.
T8b949e// Reduces BLE stack pressure and battery drain when the device is genuinely
T8b949e// out of range, while still recovering quickly from transient drops.
Tff7b72val Te6edf3backoffMs Tff7b72= Te6edf3computeReconnectBackoffMsTb4b4b4(Te6edf3consecutiveFailuresTb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Retrying in Tffd700${Te6edf3backoffMsTffd700}Ta5d6ffms (failure #Tffd700$Te6edf3consecutiveFailuresTa5d6ff)Ta5d6ff" Tb4b4b4}
Te6edf3delayTb4b4b4(Te6edf3backoffMsTb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffonConnectedTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72try Tb4b4b4{
Te6edf3bleConnectionTb4b4b4.Te6edf3deviceFlowTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tff7b72?.Te6edf3let Tb4b4b4{ Te6edf3device Tff7b72-Tff7b72>
Tff7b72val Te6edf3rssi Tff7b72= Te6edf3retryBleOperationTb4b4b4(Te6edf3tag Tff7b72= Te6edf3addressTb4b4b4) Tb4b4b4{ Te6edf3deviceTb4b4b4.Te6edf3readRssiTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Connection confirmed. Initial RSSI: Tffd700$Te6edf3rssiTa5d6ff dBmTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Failed to read initial connection RSSITa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

Tff7b72private Tff7b72fun Td2a8ffonDisconnectedTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3radioService Tff7b72= Tff7b72null

Tff7b72val Te6edf3uptime Tff7b72=
Tff7b72if Tb4b4b4(Te6edf3connectionStartTime Tff7b72> T79c0ff0Tb4b4b4) Tb4b4b4{
Te6edf3nowMillis Tff7b72- Te6edf3connectionStartTime
Tb4b4b4} Tff7b72else Tb4b4b4{
T79c0ff0
Tb4b4b4}
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{
Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] BLE disconnected - Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffUptime: Tffd700${Te6edf3uptimeTffd700}Ta5d6ffms, Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffPackets RX: Tffd700$Te6edf3packetsReceivedTa5d6ff (Tffd700$Te6edf3bytesReceivedTa5d6ff bytes), Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffPackets TX: Tffd700$Te6edf3packetsSentTa5d6ff (Tffd700$Te6edf3bytesSentTa5d6ff bytes)Ta5d6ff"
Tb4b4b4}
T8b949e// Signal DeviceSleep immediately so the UI reflects the disconnect while the
T8b949e// reconnect loop continues in the background. The previous approach suppressed
T8b949e// this signal until RECONNECT_FAILURE_THRESHOLD consecutive failures, leaving the
T8b949e// UI stuck on "Connected" for 35+ seconds after the device disappeared.
Te6edf3serviceTb4b4b4.Te6edf3onDisconnectTb4b4b4(Te6edf3isPermanent Tff7b72= Tff7b72falseTb4b4b4)
Tb4b4b4}

Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffdiscoverServicesAndSetupCharacteristicsTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72try Tb4b4b4{
Te6edf3bleConnectionTb4b4b4.Te6edf3profileTb4b4b4(Te6edf3serviceUuid Tff7b72= Te6edf3SERVICE_UUIDTb4b4b4) Tb4b4b4{ Te6edf3service Tff7b72-Tff7b72>
Tff7b72val Te6edf3radioService Tff7b72= Te6edf3serviceTb4b4b4.Te6edf3toMeshtasticRadioProfileTb4b4b4(Tb4b4b4)

T8b949e// Wire up notifications
Te6edf3radioServiceTb4b4b4.Te6edf3fromRadio
Tb4b4b4.Te6edf3onEach Tb4b4b4{ Te6edf3packet Tff7b72-Tff7b72>
Te6edf3LoggerTb4b4b4.Te6edf3v Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Received packet fromRadio (Tffd700${Te6edf3packetTb4b4b4.Te6edf3sizeTffd700}Ta5d6ff bytes)Ta5d6ff" Tb4b4b4}
Te6edf3dispatchPacketTb4b4b4(Te6edf3packetTb4b4b4)
Tb4b4b4}
Tb4b4b4.Te6edf3catch Tb4b4b4{ Te6edf3e Tff7b72-Tff7b72>
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Error in fromRadio flowTa5d6ff" Tb4b4b4}
Te6edf3handleFailureTb4b4b4(Te6edf3eTb4b4b4)
Tb4b4b4}
Tb4b4b4.Te6edf3launchInTb4b4b4(Tff7b72thisTb4b4b4)

Te6edf3radioServiceTb4b4b4.Te6edf3logRadio
Tb4b4b4.Te6edf3onEach Tb4b4b4{ Te6edf3packet Tff7b72-Tff7b72>
Te6edf3LoggerTb4b4b4.Te6edf3v Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Received packet logRadio (Tffd700${Te6edf3packetTb4b4b4.Te6edf3sizeTffd700}Ta5d6ff bytes)Ta5d6ff" Tb4b4b4}
Te6edf3dispatchPacketTb4b4b4(Te6edf3packetTb4b4b4)
Tb4b4b4}
Tb4b4b4.Te6edf3catch Tb4b4b4{ Te6edf3e Tff7b72-Tff7b72>
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Error in logRadio flowTa5d6ff" Tb4b4b4}
Te6edf3handleFailureTb4b4b4(Te6edf3eTb4b4b4)
Tb4b4b4}
Tb4b4b4.Te6edf3launchInTb4b4b4(Tff7b72thisTb4b4b4)

T8b949e// Store reference for handleSendToRadio
Tff7b72thisTf0883e@BleRadioInterface.radioService Tff7b72= Te6edf3radioService

Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Profile service active and characteristics subscribedTa5d6ff" Tb4b4b4}

T8b949e// Give Kable's async CCCD writes time to complete before triggering the
T8b949e// Meshtastic handshake. The fromRadio/logRadio observation flows register
T8b949e// notifications through the GATT queue asynchronously. Without this settle
T8b949e// window, the want_config_id burst arrives before notifications are enabled.
Te6edf3delayTb4b4b4(Te6edf3CCCD_SETTLE_MSTb4b4b4)

T8b949e// Log negotiated MTU for diagnostics
Tff7b72val Te6edf3maxLen Tff7b72= Te6edf3bleConnectionTb4b4b4.Te6edf3maximumWriteValueLengthTb4b4b4(Te6edf3BleWriteTypeTb4b4b4.Te6edf3WITHOUT_RESPONSETb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] BLE Radio Session Ready. Max write length (WITHOUT_RESPONSE): Tffd700$Te6edf3maxLenTa5d6ff bytesTa5d6ff" Tb4b4b4}

Tff7b72thisTf0883e@BleRadioInterface.service.onConnectTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Profile service discovery or operation failedTa5d6ff" Tb4b4b4}
T8b949e// Ensure the peripheral is disconnected so the outer reconnect loop sees a clean
T8b949e// Disconnected state. Do NOT call handleFailure here β€” the reconnect loop tracks
T8b949e// consecutive failures and calls handleFailure after RECONNECT_FAILURE_THRESHOLD,
T8b949e// preventing premature onDisconnect signals to the service on transient errors.
Tff7b72try Tb4b4b4{
Te6edf3bleConnectionTb4b4b4.Te6edf3disconnectTb4b4b4(Tb4b4b4)
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3ignoredTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3ignoredTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] disconnect() failed after profile errorTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

Tf0883e@Volatile Tff7b72private Tff7b72var Te6edf3radioServiceTb4b4b4: Te6edf3orgTb4b4b4.Te6edf3meshtasticTb4b4b4.Te6edf3coreTb4b4b4.Te6edf3bleTb4b4b4.Te6edf3MeshtasticRadioProfileTff7b72? Tff7b72= Tff7b72null

T8b949e// --- RadioTransport Implementation ---

T8b949e/**
* Sends a packet to the radio with retry support.
*
* @param p The packet to send.
*/
Tff7b72override Tff7b72fun Td2a8ffhandleSendToRadioTb4b4b4(Te6edf3pTb4b4b4: Te6edf3ByteArrayTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3currentService Tff7b72= Te6edf3radioService
Tff7b72if Tb4b4b4(Te6edf3currentService Tff7b72!Tff7b72= Tff7b72nullTb4b4b4) Tb4b4b4{
Te6edf3connectionScopeTb4b4b4.Te6edf3launch Tb4b4b4{
Te6edf3writeMutexTb4b4b4.Te6edf3withLock Tb4b4b4{
Tff7b72try Tb4b4b4{
Te6edf3retryBleOperationTb4b4b4(Te6edf3tag Tff7b72= Te6edf3addressTb4b4b4) Tb4b4b4{ Te6edf3currentServiceTb4b4b4.Te6edf3sendToRadioTb4b4b4(Te6edf3pTb4b4b4) Tb4b4b4}
Te6edf3packetsSentTff7b72+Tff7b72+
Te6edf3bytesSent Tff7b72+Tff7b72= Te6edf3pTb4b4b4.Te6edf3size
Te6edf3LoggerTb4b4b4.Te6edf3v Tb4b4b4{
Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Wrote packet #Tffd700$Te6edf3packetsSentTa5d6ff Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffto toRadio (Tffd700${Te6edf3pTb4b4b4.Te6edf3sizeTffd700}Ta5d6ff bytes, total TX: Tffd700$Te6edf3bytesSentTa5d6ff bytes)Ta5d6ff"
Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{
Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Failed to write packet to toRadioCharacteristic after Ta5d6ff" Tff7b72+
Ta5d6ff"Tffd700$Te6edf3packetsSentTa5d6ff successful writesTa5d6ff"
Tb4b4b4}
Te6edf3handleFailureTb4b4b4(Te6edf3eTb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] toRadio characteristic unavailable, can't send dataTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

Tf0883e@OptInTb4b4b4(Te6edf3ExperimentalAtomicApiTff7b72::Te6edf3classTb4b4b4)
Tff7b72override Tff7b72fun Td2a8ffkeepAliveTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// Send a ToRadio heartbeat so the firmware resets its power-saving idle timer.
T8b949e// The firmware only resets the timer on writes to the TORADIO characteristic; a
T8b949e// BLE-level GATT keepalive is invisible to it. Without this the device may enter
T8b949e// light-sleep and drop the BLE connection after ~60 s of application inactivity.
T8b949e//
T8b949e// Each heartbeat uses a distinct nonce to vary the wire bytes, preventing the
T8b949e// firmware's per-connection duplicate-write filter from silently dropping it.
Tff7b72val Te6edf3nonce Tff7b72= Te6edf3heartbeatNonceTb4b4b4.Te6edf3fetchAndAddTb4b4b4(T79c0ff1Tb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3v Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] BLE keepAlive β€” sending ToRadio heartbeat (nonce=Tffd700$Te6edf3nonceTa5d6ff)Ta5d6ff" Tb4b4b4}
Te6edf3handleSendToRadioTb4b4b4(Te6edf3ToRadioTb4b4b4(Te6edf3heartbeat Tff7b72= Te6edf3HeartbeatTb4b4b4(Te6edf3nonce Tff7b72= Te6edf3nonceTb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3encodeTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}

T8b949e/** Closes the connection to the device. */
Tff7b72override Tff7b72fun Td2a8ffcloseTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3uptime Tff7b72= Tff7b72if Tb4b4b4(Te6edf3connectionStartTime Tff7b72> T79c0ff0Tb4b4b4) Te6edf3nowMillis Tff7b72- Te6edf3connectionStartTime Tff7b72else T79c0ff0
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{
Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Disconnecting. Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffUptime: Tffd700${Te6edf3uptimeTffd700}Ta5d6ffms, Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffPackets RX: Tffd700$Te6edf3packetsReceivedTa5d6ff (Tffd700$Te6edf3bytesReceivedTa5d6ff bytes), Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffPackets TX: Tffd700$Te6edf3packetsSentTa5d6ff (Tffd700$Te6edf3bytesSentTa5d6ff bytes)Ta5d6ff"
Tb4b4b4}
T8b949e// Cancel the connection scope to break the while(isActive) reconnect loop.
Te6edf3connectionScopeTb4b4b4.Te6edf3cancelTb4b4b4(Ta5d6ff"Ta5d6ffclose() calledTa5d6ff"Tb4b4b4)
T8b949e// GATT cleanup must survive serviceScope cancellation. SharedRadioInterfaceService calls
T8b949e// close() and then immediately cancels serviceScope β€” a coroutine launched on serviceScope
T8b949e// may never be dispatched, leaving the BluetoothGatt object leaked (causes GATT 133 on the
T8b949e// next connect attempt). GlobalScope is the correct tool here: the cleanup is short-lived,
T8b949e// fire-and-forget, and must outlive any application-managed scope.
T8b949e// onDisconnect is handled by SharedRadioInterfaceService.stopInterfaceLocked() directly.
Tf0883e@OptInTb4b4b4(Te6edf3DelicateCoroutinesApiTff7b72::Te6edf3classTb4b4b4)
Te6edf3GlobalScopeTb4b4b4.Te6edf3launch Tb4b4b4{
Tff7b72try Tb4b4b4{
Te6edf3withTimeoutOrNullTb4b4b4(Te6edf3GATT_CLEANUP_TIMEOUTTb4b4b4) Tb4b4b4{ Te6edf3bleConnectionTb4b4b4.Te6edf3disconnectTb4b4b4(Tb4b4b4) Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffTooGenericExceptionCaughtTa5d6ff"Tb4b4b4) Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Failed to disconnect in close()Ta5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

Tff7b72private Tff7b72fun Td2a8ffdispatchPacketTb4b4b4(Te6edf3packetTb4b4b4: Te6edf3ByteArrayTb4b4b4) Tb4b4b4{
Te6edf3packetsReceivedTff7b72+Tff7b72+
Te6edf3bytesReceived Tff7b72+Tff7b72= Te6edf3packetTb4b4b4.Te6edf3size
Te6edf3LoggerTb4b4b4.Te6edf3v Tb4b4b4{
Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Dispatching packet #Tffd700$Te6edf3packetsReceivedTa5d6ff Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ff(Tffd700${Te6edf3packetTb4b4b4.Te6edf3sizeTffd700}Ta5d6ff bytes, total RX: Tffd700$Te6edf3bytesReceivedTa5d6ff bytes)Ta5d6ff"
Tb4b4b4}
Te6edf3serviceTb4b4b4.Te6edf3handleFromRadioTb4b4b4(Te6edf3packetTb4b4b4)
Tb4b4b4}

Tff7b72private Tff7b72fun Td2a8ffhandleFailureTb4b4b4(Te6edf3throwableTb4b4b4: Te6edf3ThrowableTb4b4b4) Tb4b4b4{
Tff7b72val Tb4b4b4(Te6edf3isPermanentTb4b4b4, Te6edf3msgTb4b4b4) Tff7b72= Te6edf3throwableTb4b4b4.Te6edf3toDisconnectReasonTb4b4b4(Tb4b4b4)
Te6edf3serviceTb4b4b4.Te6edf3onDisconnectTb4b4b4(Te6edf3isPermanentTb4b4b4, Te6edf3errorMessage Tff7b72= Te6edf3msgTb4b4b4)
Tb4b4b4}

Tff7b72private Tff7b72fun Te6edf3ThrowableTb4b4b4.Td2a8fftoDisconnectReasonTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3PairTff7b72<Tffa657BooleanTb4b4b4, Tffa657StringTff7b72> Tb4b4b4{
Tff7b72val Te6edf3isPermanent Tff7b72=
Tff7b72thisTff7b72::Te6edf3classTb4b4b4.Te6edf3simpleName Tff7b72=Tff7b72= Ta5d6ff"Ta5d6ffBluetoothUnavailableExceptionTa5d6ff" Tff7b72|Tff7b72|
Tff7b72thisTff7b72::Te6edf3classTb4b4b4.Te6edf3simpleName Tff7b72=Tff7b72= Ta5d6ff"Ta5d6ffManagerClosedExceptionTa5d6ff"
Tff7b72val Te6edf3msg Tff7b72=
Tff7b72when Tb4b4b4{
Tff7b72this Tff7b72is Te6edf3RadioNotConnectedException Tff7b72-Tff7b72> Tff7b72thisTb4b4b4.Te6edf3message Tff7b72?: Ta5d6ff"Ta5d6ffDevice not foundTa5d6ff"
Tff7b72this Tff7b72is Te6edf3NoSuchElementException Tff7b72|Tff7b72| Tff7b72this Tff7b72is Te6edf3IllegalArgumentException Tff7b72-Tff7b72> Ta5d6ff"Ta5d6ffRequired characteristic missingTa5d6ff"
Tff7b72thisTff7b72::Te6edf3classTb4b4b4.Te6edf3simpleName Tff7b72=Tff7b72= Ta5d6ff"Ta5d6ffGattExceptionTa5d6ff" Tff7b72-Tff7b72> Ta5d6ff"Ta5d6ffGATT Error: Tffd700${Tff7b72thisTb4b4b4.Te6edf3messageTffd700}Ta5d6ff"
Tff7b72else Tff7b72-Tff7b72> Tff7b72thisTb4b4b4.Te6edf3message Tff7b72?: Tff7b72thisTff7b72::Te6edf3classTb4b4b4.Te6edf3simpleName Tff7b72?: Ta5d6ff"Ta5d6ffUnknownTa5d6ff"
Tb4b4b4}
Tff7b72return Te6edf3PairTb4b4b4(Te6edf3isPermanentTb4b4b4, Te6edf3msgTb4b4b4)
Tb4b4b4}
Tb4b4b4}

Served by rngit 1.5.3 - Generated in 0.09s